<?php
/**
 * Unfuddle API Model
 * Base class for Unfuddle Data Models. Data models are objects within the API,
 * each with its own set of "properties". In the API these models are
 * represented by XML. That XML is converted into one of several model classes
 * defined by this package.
 *
 * @package     Unfuddle
 * @subpackage  Data Models
 * @author      Luke Visinoni <luke.visinoni@gmail.com>
 * @copyright   (c) 2010 Luke Visinoni <luke.visinoni@gmail.com>
 * @version     $Id: Abstract.php 26 2011-01-15 02:17:10Z luke.visinoni $
 * @todo        Add support for required fields
 * @todo        Add support for read-only fields
 * @todo        Add support for special/conditional fields (see Account/Attachment models)
 */
abstract class Unfuddle_Model_Abstract {

    const FORMAT_DATETIME = 'Y-m-d\TH:i:s\Z';
    
    const FORMAT_DATE = 'Y-m-d';
    
    protected $_name;
    
    protected $_fields = array(
    /*
        'update-at' => array(
            'display_name' => 'Updated At', // optional (this field would default to "Updated At" anyway)
            'type' => 'datetime', // @todo make sure this defaults to "text"
            'read-only' => true,
            'required' => true,
            'default' => null,
            'value' => null,
        ),
    */
    );
        
     /**
     * A list of this model's children. An example of a child is for messages,
     * which can have "attachments" as children.
     */
    protected $_children = array();
   
    protected $_errors = array();
    
    public function __construct($values) {
    
        foreach ($values as $field => $value) {
            $this->setProperty($field, $value);
        }
    
    }
    
    /**
     * Generate a model object from XML data.
     * @todo Return the right model (right now it does ticket no matter what).
     * @todo Sometimes multiple models will be in the xml. Need to account for that somehow.
     */
    static public function fromXml($xml) {
    
        $xmlObj = simplexml_load_string($xml);
        // determine if we have received one or multiple results
        $rootType = Unfuddle::dindex($xmlObj, 'type', 'single');
        if ($rootType == 'array') {
            $models = array();
            foreach ($xmlObj->children() as $child) {
                $models[] = self::_generateModelFromXmlObject($child);
            }
            return $models;
        } else {
            return self::_generateModelFromXmlObject($xmlObj);
        }
    
    }
    
    /**
     * @todo Use late static binding to get the name of the calling class and do
     * something like return new self();
     */
    static protected function _generateModelFromXmlObject(SimpleXMLElement $xmlObj) {
    
        $values = array();
        foreach ($xmlObj->children() as $child) {
            $type = $child['type'];
            if (is_object($type)) {
                $type = $type->__toString();
            }
            $value = (string) $child;
            if ($value != "") {
                $values[$child->getName()] = $value;
            }
        }
        $className = self::nameToClass($xmlObj->getName());
        return new $className($values);
    
    }
    
    static public function nameToClass($name) {
    
        $name = explode('-', $name);
        $name = array_map('strtolower', $name);
        $name = array_map('ucfirst', $name);
        $name = implode('', $name);
        return 'Unfuddle_Model_' . $name;
    
    }
    
    /**
     * @todo Need unit test for $validate param
     */
    public function toXml($validate = true) {
    
        if ($validate && !$this->isValid()) {
            throw Unfuddle::with(new Unfuddle_Model_ValidationException(ucfirst("{$this->_name} model is invalid and therefor cannot be converted to XML.")))
                ->setErrors($this->getErrors());
        }
        $dom = new DOMDocument('1.0', 'iso-8859-1');
        $root = $dom->createElement($this->_name);
        foreach ($this->_fields as $field => $info) {
            if (!array_key_exists('value', $info)) {
                continue;
            }
            $element = new DOMElement($field, $info['value']);
            $root->appendChild($element);
        }
        $dom->appendChild($root);
        return $dom->saveXML();
    
    }
    
    public function getProperty($name) {
    
        if (!array_key_exists($name, $this->_fields)) {
            throw new Unfuddle_Model_Exception("Attempting to retrieve non-existent '{$name}' property for the {$this->_name} model.");
        }
        if (!array_key_exists('value', $this->_fields[$name])) {
            // if the name is in the fields array, but has no value, return null
            return null;
        }
        return $this->_fields[$name]['value'];
    
    }
    
    public function setProperty($name, $value) {
    
        if (!array_key_exists($name, $this->_fields)) {
            throw new Unfuddle_Model_Exception("Attempting to set non-existent '{$name}' property for the {$this->_name} model.");
        }
        $this->_fields[$name]['value'] = $this->_convertValue($name, $value);
        return $this;
    
    }
    
    protected function _convertValue($name, $value) {
    
        if (!array_key_exists('type', $this->_fields[$name])) {
            // @todo Test this
            throw new Unfuddle_Model_Exception("{$name} property must specify its type.");
        }
        $type = $this->_fields[$name]['type'];
        switch($type) {
            case "set":
                // if field definition is an array, then the value must be in that array
                if (!array_key_exists('set', $this->_fields[$name]) || !is_array($this->_fields[$name]['set'])) {
                    // @todo test this
                    throw new Unfuddle_Model_Exception(ucfirst("{$name} is of type 'set' but does not contain set values."));
                }
                $set = $this->_fields[$name]['set'];
                if (!in_array($value, $set)) {
                    throw new Unfuddle_Model_Exception(ucfirst("{$name} property must be one of: ") . implode(", ", $set));
                }
                break;
            case "datetime":
                // "datetime" values are stored as "UTC" timestamps within 
                // Unfuddle data models to avoid mucking around with
                // timezones. Timezones should be handled by the end-user.
                // For this reason, it is important for the end-user to set
                // their timezone using date_default_timezone_set().
                $value = gmdate(self::FORMAT_DATETIME, strtotime($value));
                break;
            case "date":
                // Use date() rather than gmdate() here so that date values
                // are not converted to UTC and changed inadvertently
                $value = date(self::FORMAT_DATE, strtotime($value));
                break;
            case "integer":
                // @todo It may make more sense to throw an exception here
                // rather than blindly converting to an integer when we
                // receive invalid data...
                $value = (integer) $value;
                break;
            case "float":
                // @todo It may make more sense to throw an exception here
                // rather than blindly converting to a float when we
                // receive invalid data...
                $value = (float) $value;
                break;
            case "boolean":
                $strValue = strtolower($value);
                if (in_array($strValue, array('true', 'false'))) {
                    $value = (boolean) ($value == 'true');
                } else {
                    $value = (boolean) $value;
                }
                break;
            default:
                // Anything that is not one of the above types is assumed to
                // be a text string and can safely be cast to one.
                $value = (string) $value;
                break;
        }
        return $value;
    
    }
    
    public function isFieldRequired($field) {
    
        if (!array_key_exists($field, $this->_fields)) {
            // @todo test this
            throw new Unfuddle_Model_Exception(ucfirst("{$field} is not in this model's list of fields."));
        }
        return (boolean) (isset($this->_fields[$field]['required']) && $this->_fields[$field]['required']);
    
    }
    
    public function isFieldEmpty($field) {
    
        if (!array_key_exists($field, $this->_fields)) {
            // @todo test this
            throw new Unfuddle_Model_Exception(ucfirst("{$field} is not in this model's list of fields."));
        }
        if (array_key_exists('value', $this->_fields[$field])) {
            return (boolean) (is_null($this->_fields[$field]['value']) || $this->_fields[$field]['value'] == "");
        }
        return true;
    
    }
    
    public function isValid() {
    
        foreach ($this->_fields as $field => $info) {
            if ($this->isFieldRequired($field) && $this->isFieldEmpty($field)) {
                $this->_errors[$field] = "The {$field} property cannot be blank.";
            }
        }
        return empty($this->_errors);
    
    }
    
    public function getErrors() {
    
        return $this->_errors;
    
    }

}